// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
	"context"
	"strconv"

	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

type Type int

const (
	TypeUnknown = iota
	TypeString
	TypeInt
	TypeFloat
	TypeBool
)

// Value is a single config value.
type Value struct {
	value  string
	secure bool
	object bool
	typ    Type
}

func NewSecureValue(v string) Value {
	return Value{value: v, secure: true}
}

func NewValue(v string) Value {
	return Value{value: v, secure: false}
}

func NewTypedValue(v string, t Type) Value {
	return Value{value: v, secure: false, typ: t}
}

func NewSecureObjectValue(v string) Value {
	return Value{value: v, secure: true, object: true}
}

func NewObjectValue(v string) Value {
	return Value{value: v, secure: false, object: true}
}

// Value fetches the value of this configuration entry, using decrypter to decrypt if necessary.  If the value
// is a secret and decrypter is nil, or if decryption fails for any reason, a non-nil error is returned.
func (c Value) Value(decrypter Decrypter) (string, error) {
	if decrypter == NopDecrypter {
		return c.value, nil
	}

	ctx := context.TODO()
	obj, err := c.unmarshalObject()
	if err != nil {
		return "", err
	}
	pt, err := obj.decrypt(ctx, nil, decrypter)
	if err != nil {
		return "", err
	}
	return pt.marshalText()
}

func (c Value) Merge(base Value) (Value, error) {
	obj, err := c.unmarshalObject()
	if err != nil {
		return Value{}, err
	}
	baseObj, err := base.unmarshalObject()
	if err != nil {
		return Value{}, err
	}
	return obj.Merge(baseObj).marshalValue()
}

func (c Value) Copy(decrypter Decrypter, encrypter Encrypter) (Value, error) {
	ctx := context.TODO()
	obj, err := c.unmarshalObject()
	if err != nil {
		return Value{}, err
	}
	pt, err := obj.decrypt(ctx, nil, decrypter)
	if err != nil {
		return Value{}, err
	}
	ct, err := pt.encrypt(ctx, nil, encrypter)
	if err != nil {
		return Value{}, err
	}
	return ct.marshalValue()
}

func (c Value) SecureValues(decrypter Decrypter) ([]string, error) {
	ctx := context.TODO()
	obj, err := c.unmarshalObject()
	if err != nil {
		return nil, err
	}

	var ctChunks [][]string
	obj.EncryptedValues(&ctChunks)

	var result []string
	for _, ctChunk := range ctChunks {
		if len(ctChunk) == 0 {
			continue
		}
		decryptedChunk, err := decrypter.BatchDecrypt(ctx, ctChunk)
		if err != nil {
			return nil, err
		}
		result = append(result, decryptedChunk...)
	}
	return result, nil
}

func (c Value) Secure() bool {
	return c.secure
}

func (c Value) Object() bool {
	return c.object
}

// coerceObject returns a more suitable value for objects by converting untyped string configuration values into
// boolean or number values.
func (c Value) coerceObject() (object, error) {
	// If it's a secure value, a typed value, or an object, return as-is.
	if c.Secure() || c.Object() || c.typ != TypeUnknown {
		return c.unmarshalObject()
	}

	// Otherwise, attempt to coerce the value into a boolean or a number.
	coerced, ok := coerce(c.value)
	if !ok {
		return c.unmarshalObject()
	}
	switch coerced := coerced.(type) {
	case bool:
		return newObject(coerced), nil
	case int64:
		return newObject(coerced), nil
	case uint64:
		return newObject(coerced), nil
	default:
		contract.Failf("unreachable")
		return object{}, nil
	}
}

func (c Value) unmarshalObject() (object, error) {
	if c.secure || c.object || c.typ == TypeUnknown {
		var obj object
		err := obj.UnmarshalString(c.value, c.secure, c.object)
		return obj, err
	}

	switch c.typ {
	case TypeString:
		return newObject(c.value), nil
	case TypeInt:
		i, err := strconv.Atoi(c.value)
		if err != nil {
			return object{}, err
		}
		return newObject(int64(i)), nil
	case TypeBool:
		return newObject(c.value == "true"), nil
	case TypeFloat:
		f, err := strconv.ParseFloat(c.value, 64)
		if err != nil {
			return object{}, err
		}
		return newObject(f), nil
	default:
		contract.Failf("unreachable")
		return object{}, nil
	}
}

// ToObject returns the string value (if not an object), or the unmarshalled JSON object (if an object).
func (c Value) ToObject() (any, error) {
	obj, err := c.unmarshalObject()
	if err != nil {
		return nil, err
	}
	return obj.marshalObjectValue(true), nil
}

func (c Value) MarshalJSON() ([]byte, error) {
	obj, err := c.unmarshalObject()
	if err != nil {
		return nil, err
	}
	return obj.MarshalJSON()
}

func (c *Value) UnmarshalJSON(b []byte) (err error) {
	var obj object
	if err = obj.UnmarshalJSON(b); err != nil {
		return err
	}
	c.value, c.secure, c.object, err = obj.MarshalString()
	return err
}

func (c Value) MarshalYAML() (any, error) {
	obj, err := c.unmarshalObject()
	if err != nil {
		return "", err
	}
	return obj.MarshalYAML()
}

func (c *Value) UnmarshalYAML(unmarshal func(any) error) (err error) {
	var obj object
	if err = obj.UnmarshalYAML(unmarshal); err != nil {
		return err
	}
	c.value, c.secure, c.object, err = obj.MarshalString()
	return err
}
